package com.mvu.banana.tools;

import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.SpanElement;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.*;
import com.mvu.banana.common.client.UIVar;
import com.mvu.banana.common.client.UiIn;
import com.mvu.banana.common.client.UiOut;
import com.mvu.banana.common.client.ValueDTO;
import com.mvu.banana.common.server.AJAXServlet;
import com.mvu.banana.common.shared.InterfaceFor;
import com.mvu.banana.common.widget.HintListener;
import com.mvu.banana.domain.client.MyConstants;
import com.mvu.banana.guest.client.SignUpForm;
import com.mvu.banana.user.client.SearchProfile;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;

import static com.mvu.banana.tools.GeneratorUtil.getFieldName;

/**
 */
public class DtoGenerator extends CodeGenerator{
  static Set<String> availableConstants;

  static {
    availableConstants = new HashSet<String>();
    for (Method method : MyConstants.class.getDeclaredMethods()) {
      availableConstants.add(method.getName());
    }
  }

  private static String dtoName;
  private static final String HintListenerClass = HintListener.class.getSimpleName();

  public static void main(String[] args) throws Exception {
    genDto(SignUpForm.class);
    genDto(SearchProfile.class);
  }

  private static Class sourceClass;
  private static JavaClass javaClass;
  private static HashMap<String, Class> varTypes = new HashMap<String, Class>();
  private static Comparator<Field> fieldComparator = new Comparator<Field>() {
    @Override
    public int compare(Field o1, Field o2) {
      return o1.getName().compareTo(o2.getName());
    }
  };
  private static Comparator<Method> methodComparator = new Comparator<Method>() {
    @Override
    public int compare(Method o1, Method o2) {
      return o1.getName().compareTo(o2.getName());
    }
  };
  private static TreeMap<Field, Class> inputs = new TreeMap<Field, Class>(fieldComparator);
  private static TreeMap<Method, Class> inputMethods = new TreeMap<Method, Class>(methodComparator);
  private static TreeMap<Field, Class> outputs = new TreeMap<Field, Class>(fieldComparator);
  private static TreeMap<Method, Class> outputMethods = new TreeMap<Method, Class>(methodComparator);

  static void genDto(Class srcClass) throws Exception {
    reset();
    sourceClass = srcClass;
    dtoName = sourceClass.getName().replaceFirst(".client.", ".generated.") + "DTO";
    javaClass = new JavaClass().withName(dtoName).withSuperClass(ValueDTO.class, sourceClass.getSimpleName());
    javaClass.importClass(sourceClass);
    JavaMethod noArgsConstructor = javaClass.addConstructor();
    HashMap<String, JavaMethod> updateMethods = new HashMap<String, JavaMethod>();
    HashMap<String, JavaMethod> copyMethods = new HashMap<String, JavaMethod>();
    Field[] declaredFields = sourceClass.getDeclaredFields();
    for (Field field : declaredFields) {
      String fieldName = field.getName();
      Class<?> inputType = field.getType();
      UIVar uiVar = field.getAnnotation(UIVar.class);
      if (uiVar != null) {
        javaClass.addField().withModifier("public").withName(fieldName + "Id")
          .withType(Long.class);
        JavaMethod update = javaClass.addMethod(false, "void", "update", inputType);
        updateMethods.put(fieldName, update);
        JavaMethod copy = javaClass.addMethod(false, sourceClass.getSimpleName() + "DTO", "copy", inputType);
        copy.addLine("this.", fieldName, "Id = p0.getId()");
        copyMethods.put(fieldName, copy);
        InterfaceFor interfaceFor = inputType.getAnnotation(InterfaceFor.class);
        Class beanType = Class.forName(interfaceFor.value());
        varTypes.put(fieldName, beanType);
      }
      UiIn uiIn = field.getAnnotation(UiIn.class);
      UiOut uiOut = field.getAnnotation(UiOut.class);
      if (uiIn != null || uiOut != null) {
        Field beanField = getBeanField(field);
        Class<?> type = uiIn == null ? uiOut.type() : uiIn.type();
        Class fieldType = getFieldType(beanField, inputType, type);
        javaClass.addField("public", fieldName, fieldType, null);
        if (uiIn != null) {
          inputs.put(field, fieldType);
        }
        if (uiOut != null) {
          outputs.put(field, fieldType);
        }
      }
    }
    for (Method method : sourceClass.getDeclaredMethods()) {
      UiOut uiOut = method.getAnnotation(UiOut.class);
      UiIn uiIn = method.getAnnotation(UiIn.class);
      if (uiOut != null || uiIn != null) {
        Class fieldType = uiOut != null ? method.getParameterTypes()[0] : method.getReturnType();
        String fieldName = method.getName().substring(3);
        javaClass.addField("public", fieldName, fieldType, null);
        if (uiIn != null) {
          inputMethods.put(method, fieldType);
        }
        if (uiOut != null) {
          outputMethods.put(method, fieldType);
        }
      }
    }
    generatePrepareMethod();
    generateValidateMethod();
    generateUpdateMethod();
    generateCopyMethod();
    generateUpdateBeanMethod(updateMethods);
    generateCopyFromBeanMethod(copyMethods);
    javaClass.addToStringMethod();
    CodeGenerator.logger.info(javaClass.print());
    javaClass.writeToMain(true);
    generateServlet();
  }

  private static void generateServlet() {
    String name = sourceClass.getName().replaceFirst(".(client|widget).", ".server.") + "Servlet";
    String dtoSimpleName = sourceClass.getSimpleName() + "DTO";
    JavaClass servletClass = new JavaClass().withName(name).withSuperClass(AJAXServlet.class, dtoSimpleName);
    servletClass.importClass(dtoName);
    JavaMethod post = servletClass.addMethod(false, dtoSimpleName, "post", dtoSimpleName);
    post.addLine("return p0");
    servletClass.writeToMain(false);
  }

  private static void reset() {
    varTypes.clear();
    inputs.clear();
    outputs.clear();
    inputMethods.clear();
    outputMethods.clear();
  }

  private static void generatePrepareMethod() {
    JavaMethod prepareMethod = javaClass.addMethod(false, "void", "prepare", sourceClass);
    for (Map.Entry<Field, Class> entry : inputs.entrySet()) {
      Field field = entry.getKey();
      String fieldName = field.getName();
      Class<?> type = entry.getValue();
      Class<?> widgetType = field.getType();
      if(Widget.class.isAssignableFrom(widgetType)
              && field.getAnnotation(UiField.class) == null){ // not initialize by binder
        javaClass.importClass(widgetType);
        prepareMethod.addLine("p0.", fieldName, " = new ", widgetType.getSimpleName(), "()");
      }
      if (HasName.class.isAssignableFrom(widgetType)) {
        prepareMethod.addLine("p0.", fieldName, ".setName(\"", fieldName, "\")");
      }
      if (type.isEnum()) {
        String simpleName = type.getSimpleName();
        prepareMethod.addLine("for(", simpleName, " e : ", simpleName, ".values()){\n      p0.",
                fieldName, ".addItem(p0.getStringValue(e), e.name());\n    }");
      }
      String title = new StringBuilder(sourceClass.getSimpleName()).append("_").append(fieldName).toString();
      if (availableConstants.contains(title)) {
        javaClass.importClass(HintListener.class);
        prepareMethod.addLine("p0.", fieldName, ".setTitle(\"", title, "\")");
        prepareMethod.addLine("p0.", fieldName, ".addFocusHandler(", HintListenerClass, ".get())");
        prepareMethod.addLine("p0.", fieldName, ".addBlurHandler(", HintListenerClass, ".get())");
      }
    }
  }

  private static void generateCopyFromBeanMethod(HashMap<String, JavaMethod> copyMethods) {
    for (Field field : inputs.keySet()) {
      UiIn uiIn = field.getAnnotation(UiIn.class);
      String var = uiIn.var();
      if (var.length() > 0) {
        JavaMethod copy = copyMethods.get(var);
        String fieldName = field.getName();
        copy.addLine("this.", fieldName, " = p0.get", getFieldName(uiIn.path()), "()");
      }
    }
    for (JavaMethod copy : copyMethods.values()) {
      copy.addLine("return this");
    }
  }

  private static void generateUpdateBeanMethod(HashMap<String, JavaMethod> updateMethods) {
    for (Field field : inputs.keySet()) {
      UiIn uiIn = field.getAnnotation(UiIn.class);
      String var = uiIn.var();
      if (var.length() > 0) {
        JavaMethod update = updateMethods.get(var);
        String fieldName = field.getName();
        update.addLine("p0.set", getFieldName(uiIn.path()), "(this.", fieldName, ")");
      }
    }
  }

  private static void generateCopyMethod() {
    JavaMethod copyValuesMethod = null;
    for (Field field : inputs.keySet()) {
      String fieldName = field.getName();
      String getValueExpression = getValueExpression(field);
      if (getValueExpression != null) {
        if (copyValuesMethod == null) {
          copyValuesMethod = javaClass.addMethod(false, "void", "copy", sourceClass);
        }
        copyValuesMethod.addLine("this.", fieldName, " = ", getValueExpression);
      }
    }
    for (Method method : inputMethods.keySet()) {
      String fieldName = method.getName().substring(3);
      if (copyValuesMethod == null) {
        copyValuesMethod = javaClass.addMethod(false, "void", "copy", sourceClass);
      }
      copyValuesMethod.addLine("this.", fieldName, " = p0.", method.getName() + "()");
    }
  }

  private static void generateUpdateMethod() {
    JavaMethod updateMethod = javaClass.addMethod(false, "void", "update", sourceClass);
    for (Field field : outputs.keySet()) {
      String fieldName = field.getName();
      updateMethod.addLine("p0.", fieldName, setValueExpression(field));
    }
    for (Method method : outputMethods.keySet()) {
      String fieldName = method.getName().substring(3);
      updateMethod.addLine("p0.", method.getName(), "(this.", fieldName, ")");
    }
  }

  private static void generateValidateMethod() {
    JavaMethod validateMethod = null;
    for (Field field : inputs.keySet()) {
      String fieldName = field.getName();
      String checkExpression = getCheckExpression(field);
      if (checkExpression != null) {
        if (validateMethod == null) {
          javaClass.importClass("static com.mvu.banana.common.validation.Validator.*");
          validateMethod = javaClass.addMethod(false, "boolean", "validate", sourceClass);
          validateMethod.addLine("boolean isValid = true");
        }
        validateMethod.addLine("boolean ", fieldName, "IsValid = ", checkExpression);
        validateMethod.addLine("p0.highlight(p0.", fieldName, ", ", fieldName, "IsValid)");
        validateMethod.addLine("isValid &= ", fieldName, "IsValid");
      }
    }
    if (validateMethod != null) {
      validateMethod.addLine("return isValid");
    }
  }

  private static String getCheckExpression(Field field) {
    Field beanField = getBeanField(field);
    Annotation[] fieldAnnotations = field.getAnnotations();
    Annotation[] beanAnnotations = beanField == null ? new Annotation[0] : beanField.getAnnotations();
    int constraintsCount = 0;
    StringBuilder builder = new StringBuilder();
    int i = -1;
    int total = fieldAnnotations.length + beanAnnotations.length;
    while (++i < total) {
      Annotation annotation = i < fieldAnnotations.length ? fieldAnnotations[i]
              : beanAnnotations[i - fieldAnnotations.length];
      Class annotationClass = annotation.annotationType();
      String className = annotationClass.getName();
      if (className.contains("constraints")) {
        if (constraintsCount > 0) {
          builder.append(" && ");
        }
        builder.append("is").append(annotationClass.getSimpleName()).append("(")
                .append(field.getName());
        if (annotationClass.equals(Min.class)) {
          builder.append(", ").append(((Min) annotation).value());
        } else if (annotationClass.equals(Max.class)) {
          builder.append(", ").append(((Max) annotation).value());
        } else if (annotationClass.equals(Length.class)) {
          Length length = (Length) annotation;
          builder.append(", ").append(length.min()).append(", ").append(length.max());
        }
        builder.append(")");
        constraintsCount++;
      }
    }
    return constraintsCount > 0 ? builder.toString() : null;
  }

  private static String setValueExpression(Field field) {
    String fieldName = field.getName();
    Class inputType = field.getType();
    Class fieldType = inputs.get(field);
    StringBuilder builder = new StringBuilder();
    if (HasValue.class.isAssignableFrom(inputType)) {
      builder.append(".setValue(this.").append(fieldName).append(")");
    } else if (SpanElement.class.isAssignableFrom(inputType)) {
      builder.append(".setInnerText(this.").append(fieldName).append(")");
    } else if (ListBox.class.isAssignableFrom(inputType)) {
      builder.append(".setSelectedIndex(this.").append(fieldName);
      if (fieldType.isEnum()) {
        builder.append(".ordinal() + 1)");
      } else {
        builder.append(")");
      }
    } else {
      builder.append(" = this.").append(fieldName);
    }
    return builder.toString();
  }

  /**
   * Return null if is not user input field
   */
  private static String getValueExpression(Field field) {
    String fieldName = field.getName();
    Class inputType = field.getType();
    Class fieldType = inputs.get(field);
    StringBuilder builder = new StringBuilder();
    if (HasValue.class.isAssignableFrom(inputType)) {
      builder.append("p0.").append(fieldName).append(".getValue()");
    } else if (SpanElement.class.isAssignableFrom(inputType)) {
      return null; // this is not user input field
    } else if (ListBox.class.isAssignableFrom(inputType)) {
      builder.append(fieldType.getSimpleName()).append(".valueOf(")
              .append("p0.").append(fieldName).append(".getValue(p0.")
              .append(fieldName).append(".getSelectedIndex()))");
    } else {
      builder.append("p0.").append(fieldName);
    }
    return builder.toString();
  }

  private static Class getFieldType(Field beanField, Class inputType, Class<?> type) {
    if (!Void.class.equals(type)) {
      return type;
    }
    if (beanField != null) {
      return beanField.getType();
    }
    if (TextBoxBase.class.isAssignableFrom(inputType)) {
      return String.class;
    } else if (Element.class.isAssignableFrom(inputType)) {
      return String.class;
    } else if (ListBox.class.isAssignableFrom(inputType)) {
      return Integer.class;
    } else if (CheckBox.class.isAssignableFrom(inputType)) {
      return Boolean.class;
    } else {
      return inputType;
    }
  }

  private static Field getBeanField(Field field) {
    UiIn uiIn = field.getAnnotation(UiIn.class);
    if (uiIn != null && !uiIn.var().isEmpty()) {
      Class varType = varTypes.get(uiIn.var());
      try {
        return varType.getDeclaredField(uiIn.path());
      } catch (NoSuchFieldException e) {
        e.printStackTrace();
        throw new RuntimeException(e);
      }
    }
    return null;
  }

  public static void print(Object... messages) {
    for (Object message : messages) {
      System.out.print(message);
    }
    System.out.println();
  }
}
